package com.leyou.search.service;

import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.fasterxml.jackson.core.type.TypeReference;
import com.leyou.common.exception.pojo.ExceptionEnum;
import com.leyou.common.exception.pojo.LyException;
import com.leyou.common.pojo.PageResult;
import com.leyou.common.utils.BeanHelper;
import com.leyou.common.utils.HighlightUtils;
import com.leyou.common.utils.JsonUtils;
import com.leyou.item.client.ItemClient;
import com.leyou.item.dto.SpuDTO;
import com.leyou.item.pojo.*;
import com.leyou.search.dto.GoodsDTO;
import com.leyou.search.dto.SearchRequest;
import com.leyou.search.dto.SearchResult;
import com.leyou.search.pojo.Goods;
import com.leyou.search.repository.SearchRepository;
import org.apache.commons.lang.StringUtils;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage;
import org.springframework.data.elasticsearch.core.query.FetchSourceFilter;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.stream.Collectors;

/**
 * 搜索业务
 */
@Service
public class SearchService {
    @Autowired
    private SearchRepository searchRepository;//完成CRUD操作
    @Autowired
    private ElasticsearchTemplate esTemplate;//用于高级搜索
    @Autowired
    private ItemClient itemClient;
    /**
     * 数据导入(MySQL->ES)
     */
    public void importData(){
        int page = 1;
        int rows = 100;
        long totalPage = 1;
        //1.分页查询Spu
        while (page<=totalPage){
            //注意：只需要导入上架商品
            // key为null，表示要查找数据库所有的数据
            PageResult<SpuDTO> pageResult = itemClient.spuPageQuery(page,rows,null,true);

            //导入数据
            List<SpuDTO> spuDTOList = pageResult.getItems();

            if(CollectionUtils.isNotEmpty(spuDTOList)){
                //把List<SpuDTO>转换为List<Goods>
                //map():把一个SpuDTO对象转换为一个Goods对象
                List<Goods> goodsList = spuDTOList.stream().map(spuDTO -> buildGoods(spuDTO)).collect(Collectors.toList());
                searchRepository.saveAll(goodsList);
                totalPage = pageResult.getTotalPage();//获取总页数
                page++;
            }
        }
    }

    /**
     * 把一个SpuDTO对象转换为一个Goods对象
     * @param spuDTO
     * @return
     */
    public Goods buildGoods(SpuDTO spuDTO) {
        Goods goods = new Goods();

        //一、处理all，skus，price属性
        List<Sku> skuList = itemClient.findSkusBySpuId(spuDTO.getId());

        //设计一个List集合，存入所有需要显示的Sku数据
        List<Map<String,Object>> skuMapList = new ArrayList<>();
        if(CollectionUtils.isNotEmpty(skuList)){
            skuList.forEach(sku -> {
                Map<String,Object> skuMap = new HashMap<>();
                skuMap.put("id",sku.getId());
                skuMap.put("price",sku.getPrice());
                skuMap.put("images",sku.getImages());
                skuMapList.add(skuMap);
            });
        }

        String all = spuDTO.getName()+spuDTO.getSubTitle()+skuList.stream().map(Sku::getTitle).collect(Collectors.joining());

        Set<Long> price = skuList.stream().map(Sku::getPrice).collect(Collectors.toSet());


        //二、处理specs属性
        /**
         * specs的格式：Map
         *    通用规格参数： < "SpecParam的name" , "SpuDetail的genericSpec(只有一个值)"  >
         *    特殊规格参数： < "SpecParam的name" , "SpuDetail的specialSpec(可能有多个值)"  >
         */
        Map<String,Object> specs = new HashMap<>();
        //1）根据分类ID查询用于搜索的规格参数
        List<SpecParam> specParams = itemClient.findSpecParams(null, spuDTO.getCid3(), true);

        //根据spuId查询SpuDetail对象
        SpuDetail spuDetail = itemClient.findSpuDetailBySpuId(spuDTO.getId());
        //获取通用规格参数值 及 特殊规格参数列表
        String genericSpec = spuDetail.getGenericSpec();
        String specialSpec = spuDetail.getSpecialSpec();

        //把通用规格参数值 及 特殊规格参数列表字符串转换为Map集合
        //将字符串转换为Map集合
        Map<Long, Object> genericSpecMap = JsonUtils.toMap(genericSpec, Long.class, Object.class);//toMap：只能转换一层json结构
        //nativeRead：可以定义转换多层json结构
        Map<Long, List<Object>> specialSpecMap = JsonUtils.nativeRead(specialSpec, new TypeReference<Map<Long, List<Object>>>() {});
        //2）判断该参数是否为通用规格参数
        specParams.forEach(specParam -> {
            String key = specParam.getName();
            Object value = null;

            //3）分别处理 通用规格参数 和 特殊规格参数
            // value 是spu_detail的generic_spec或special_spec的值
            if(specParam.getGeneric()){
                //通用规格参数
                value = genericSpecMap.get(specParam.getId());
            }else{
                //特殊规格参数
                value = specialSpecMap.get(specParam.getId());
            }

            //判断参数类型是否为数字，如果是，转换为数字区间
            if(specParam.getNumeric()){
                value = chooseSegment(value,specParam);
            }

            //4）封装Map集合
            specs.put(key,value);
        });

        goods.setId(spuDTO.getId());
        goods.setSpuName(spuDTO.getName());
        goods.setSubTitle(spuDTO.getSubTitle());
        goods.setCreateTime(spuDTO.getCreateTime().getTime());
        goods.setCategoryId(spuDTO.getCid3());
        goods.setBrandId(spuDTO.getBrandId());
        goods.setAll(all);
        goods.setPrice(price);
        goods.setSkus(JsonUtils.toString(skuMapList));
        goods.setSpecs(specs);
        return goods;
    }
    /**
     * 把一个具体数字转换为数字区间
     * @param value 转换后的区间
     * @param specParam
     * @return
     */
    public String chooseSegment(Object value, SpecParam specParam) {
        // value 是spu_detail的generic_spec或special_spec的值
        if (value == null || StringUtils.isBlank(value.toString())) {
            return "其它";
        }
        double val = parseDouble(value.toString());
        String result = "其它";
        // 保存数值段
        for (String segment : specParam.getSegments().split(",")) {
            String[] segs = segment.split("-");
            // 获取数值范围
            double begin = parseDouble(segs[0]);
            double end = Double.MAX_VALUE;
            if (segs.length == 2) {
                end = parseDouble(segs[1]);
            }
            // 判断是否在范围内
            if (val >= begin && val < end) {
                if (segs.length == 1) {
                    result = segs[0] + specParam.getUnit() + "以上";
                } else if (begin == 0) {
                    result = segs[1] + specParam.getUnit() + "以下";
                } else {
                    result = segment + specParam.getUnit();
                }
                break;
            }
        }
        return result;
    }

    private double parseDouble(String str) {
        try {
            return Double.parseDouble(str);
        } catch (Exception e) {
            return 0;
        }
    }

    public SearchResult<GoodsDTO> goodsSearchPage(SearchRequest searchRequest) {
        // 1.创建SearchResult对象
        SearchResult<GoodsDTO> searchResult = new SearchResult<>();

        //2.封装SearchResult对象
        //2.1 查询商品分页列表
        PageResult<GoodsDTO> pageResult = itemQueryPage(searchRequest);

        //2.2 查询过滤条件
        Map<String,Object> filterConditions = filterConditionsQuery(searchRequest);

        searchResult.setItems(pageResult.getItems());
        searchResult.setTotal(pageResult.getTotal());
        searchResult.setTotalPage(pageResult.getTotalPage());
        searchResult.setFilterConditions(filterConditions);

        //3.返回SearchResult对象
        return searchResult;
    }

    /**
     * 查询过滤条件
     * @param searchRequest
     * @return
     */
    public Map<String, Object> filterConditionsQuery(SearchRequest searchRequest) {
        //1.创建Map集合，用于存储所有的过滤条件
        Map<String,Object> filterConditionsMap = new LinkedHashMap<>();//注意：为了过滤条件有序地展示

        //2.封装Map集合所有过滤条件

        //2.1 固定过滤条件（分类和品牌）
        NativeSearchQueryBuilder queryBuilder = createNativeQueryBuilder(searchRequest);

        //1）追加结果过滤
        queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{""},null));

        //2）追加聚合条件
        //定义聚合别名
        String categoryAgg = "categoryAgg";
        String brandAgg = "brandAgg";
        queryBuilder.addAggregation(AggregationBuilders.terms(categoryAgg).field("categoryId"));
        queryBuilder.addAggregation(AggregationBuilders.terms(brandAgg).field("brandId"));

        //3）执行聚合查询
        //AggregatedPage: 包含聚合结果
        AggregatedPage<Goods> aggregatedPage = esTemplate.queryForPage(queryBuilder.build(),Goods.class);

        //4）获取所有聚合结果
        Aggregations aggregations = aggregatedPage.getAggregations();

        //5）获取分类的聚合结果
        Terms terms = aggregations.get(categoryAgg);
        //获取分类的桶
        List<Long> categoryIds = terms.getBuckets()
                .stream()
                .map(Terms.Bucket::getKeyAsNumber) // 以Number类型取出key值
                .map(Number::longValue) //把Number类型转换Long类型
                .collect(Collectors.toList());
        List<Category> categoryList = itemClient.findCategoriesByIds(categoryIds);

        //6）把分类聚合结果存入Map集合
        filterConditionsMap.put("分类",categoryList);

        //7）获取品牌的聚合结果，并且存入Map集合
        Terms brandTerms = aggregations.get(brandAgg);
        //获取分类的桶
        List<Long> brandIds = brandTerms.getBuckets()
                .stream()
                .map(Terms.Bucket::getKeyAsNumber) // 以Number类型取出key值
                .map(Number::longValue) //把Number类型转换Long类型
                .collect(Collectors.toList());
        List<Brand> brandList = itemClient.findBrandsByIds(brandIds);
        filterConditionsMap.put("品牌",brandList);

        //2.2 动态过滤条件（规格参数）

        if(categoryIds!=null){
            categoryIds.forEach(categoryId->{
                //1）使用固定过滤条件中的所有分类，查询用于搜索的规格参数
                List<SpecParam> specParams = itemClient.findSpecParams(null, categoryId, true);

                //2）使用第一步得到的规格参数进行聚合查询
                if(specParams!=null){
                    specParams.forEach(specParam -> {
                        queryBuilder.addAggregation(AggregationBuilders.terms(specParam.getName()).field("specs."+specParam.getName()+".keyword"));
                    });
                }

                //执行聚合查询，获取结果
                AggregatedPage specParamAggregatedPage = esTemplate.queryForPage(queryBuilder.build(),Goods.class);

                //3）把聚合结果放入Map集合
                Aggregations specParamAggregations = specParamAggregatedPage.getAggregations();

                if(specParams!=null){
                    specParams.forEach(specParam -> {
                        //根据聚合别名取出聚合结果
                        Terms specParamTerms = specParamAggregations.get(specParam.getName());
                        //取出结果
                        List<String> specParamList = specParamTerms.getBuckets()
                                .stream()
                                .map(Terms.Bucket::getKeyAsString)
                                .collect(Collectors.toList());
                        //把参数聚合结果存入Map集合
                        filterConditionsMap.put(specParam.getName(),specParamList);
                    });
                }

            });
        }

        //3.返回Map集合
        return filterConditionsMap;
    }

    /**
     * 抽取共用代码:创建本地构造器
     * @param searchRequest
     * @return
     */
    private NativeSearchQueryBuilder createNativeQueryBuilder(SearchRequest searchRequest) {
        //1.创建本地查询构造器对象
        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();

        //2.往本地查询构造器对象添加条件
        //2.1 添加query条件
        //1）创建布尔查询构造器
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();

        //2）追加must条件
        boolQueryBuilder.must(QueryBuilders.multiMatchQuery(searchRequest.getKey(),"all","spuName"));

        //追加布尔的过滤条件
        //1)取出所有条件
        Map<String,Object> filterParams = searchRequest.getFilterParams();
        //2)追加条件
        if(filterParams != null){
            filterParams.entrySet().forEach(entry->{
                String key = entry.getKey();
                Object value = entry.getValue();

                //处理key
                if(key.equals("分类")){
                    key = "categoryId";
                }else if(key.equals("品牌")){
                    key = "brandId";
                }else{
                    key = "specs." + key + ".keyword";
                }

                boolQueryBuilder.filter(QueryBuilders.termQuery(key,value));
            });
        }

        queryBuilder.withQuery(boolQueryBuilder);
        return queryBuilder;
    }


    /**
     * 查询搜索商品分页列表
     * @param searchRequest
     * @return
     */
    public PageResult<GoodsDTO> itemQueryPage(SearchRequest searchRequest) {
        //1.创建本地查询构造器对象
        NativeSearchQueryBuilder queryBuilder = createNativeQueryBuilder(searchRequest);

        //2.2 添加结果过滤
        queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{"id","spuName","subTitle","skus"},null));

        //2.3 添加分页条件
        //注意：参数一代表当前页码，是从0开始的
        queryBuilder.withPageable(PageRequest.of(searchRequest.getPage() - 1,searchRequest.getSize()));
        // 2.4增加高亮字段
        HighlightUtils.highlightField(queryBuilder,"spuName");

        //3.执行本地查询，获取结果
        Page<Goods> pageBean = esTemplate.queryForPage(queryBuilder.build(), Goods.class,
                HighlightUtils.highlightBody(Goods.class,"spuName"));

        //4.处理结果，返回结果
        List<Goods> goodsList = pageBean.getContent();

        //拷贝数据（注意：只拷贝同名属性）
        List<GoodsDTO> goodsDTOList = BeanHelper.copyWithCollection(goodsList,GoodsDTO.class);
        return new PageResult<>(pageBean.getTotalElements(),Long.valueOf(pageBean.getTotalPages()),goodsDTOList);
    }

    /**
     * 只要当页的数据
     * @param searchRequest
     * @return
     */
    public List<GoodsDTO> goodsPageChange(SearchRequest searchRequest) {
        // PageResult<GoodsDTO> pageResult = this.itemQueryPage(searchRequest);
        // return pageResult.getItems();
        // 只需要getItem()是因为前端只需要Items的信息，itemQueryPage查询的是页面中所有的数据，而goodsPageChange只需要查询Items。
        return this.itemQueryPage(searchRequest).getItems();
    }

    /**
     * 根据商品上下架，新增或删除es索引库的索引
     * @param spuId
     */
    public void createIndex(Long spuId) {

        try {
            //1.根据spuId查询SpuDTO
            SpuDTO spuDTO = itemClient.findSpuById(spuId);
            //2.把SpuDTO转换为Goods
            Goods goods = this.buildGoods(spuDTO);
            //3.保存Goods
            searchRepository.save(goods);
        } catch (Exception e) {
            e.printStackTrace();
            throw new LyException(ExceptionEnum.INDEX_INSERT_ERROR);
        }
    }

    public void deleteIndex(Long spuId) {

        try {
            searchRepository.deleteById(spuId);
        } catch (Exception e) {
            e.printStackTrace();
            throw new LyException(ExceptionEnum.INDEX_DELETE_ERROR);
        }
    }
}